/*
 * Copyright (c) 2021 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the 'License');
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an 'AS IS' BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { Route } from '../Route';
import { Address } from '../Address';
import { Proxy, ProxyType } from '../Socket/Proxy';
import List from '@ohos.util.List';
import { kotlinRequire, Utils } from '../utils/Utils';
import { RouteDatabase } from './RouteDatabase';
import { EventListener } from '../EventListener'
import { Call } from '../Call';
import { SocketAddress } from '../Socket/SocketAddress';
import { HttpUrl } from '../HttpUrl';
import { SocketException, UnknownHostException, NoSuchElementException } from '../Error/error';

export class RouteSelector {
    private address: Address
    private routeDatabase: RouteDatabase
    private call: Call
    private eventListener: EventListener

    constructor(address: Address,routeDatabase: RouteDatabase,call:Call,eventListener:EventListener) {
        this.address = address
        this.routeDatabase = routeDatabase
        this.call = call
        this.eventListener = eventListener
        this.resetNextProxy(address.url, address.proxy)
    }
    /* State for negotiating the next proxy to use. */
    private proxies = new List<Proxy>()
    private nextProxyIndex: number = 0

    /* State for negotiating the next socket address to use. */
    private inetSocketAddresses = new List<SocketAddress>()

    /* State for negotiating failed routes */
    private postponedRoutes = new List<Route>()


    /**
     * Returns true if there's another set of routes to attempt. Every address has at least one route.
     */
    hasNext():boolean {
        return this.hasNextProxy() || !this.postponedRoutes.isEmpty()
    }

    async next():Promise<Selection>{
        if(!this.hasNext()) throw new NoSuchElementException('')
        let routes = new List<Route>();
        while (this.hasNextProxy()) {
            // Postponed routes are always tried last. For example, if we have 2 proxies and all the
            // routes for proxy1 should be postponed, we'll move to proxy2. Only after we've exhausted
            // all the good routes will we attempt the postponed routes.
            let proxy = await this.nextProxy()
            for (let i = 0; i < this.inetSocketAddresses.length; i++) {
                let inetSocketAddress = this.inetSocketAddresses[i]
                let route = new Route(this.address, proxy, inetSocketAddress);
                if (this.routeDatabase.shouldPostpone(route)) {
                    this.postponedRoutes.add(route)
                } else {
                    routes.add(route)
                }
            }
            if(!routes.isEmpty()) {
                break
            }
        }
        if(routes.isEmpty()) {
            // We've exhausted all Proxies so fallback to the postponed routes.
            this.postponedRoutes.forEach(element => {
                routes.add(element)
            });
            this.postponedRoutes.clear()
        }
        return new Selection(routes);
    }

    /** Prepares the proxy servers to try. */
    private resetNextProxy(url: HttpUrl, proxy?: Proxy) {
        let selectProxies = (): List<Proxy> => {
            // If the user specifies a proxy, try that and only that.
            if (proxy !== null && proxy !== undefined){
                let list = new List<Proxy>()
                list.add(proxy)
                return list
            }

            // If the URI lacks a host (as in "http://</"), don't call the ProxySelector.
            let uri = url.toUri()
            if (uri.host === null || uri.host === undefined){
                let list = new List<Proxy>()
                list.add(Proxy.NO_PROXY)
                return list
            }

            // Try each of the ProxySelector choices until one connection succeeds.
            let proxiesOrNull = this.address.proxySelector.select(uri)
            if (!proxiesOrNull || proxiesOrNull.isEmpty()){
                let list = new List<Proxy>()
                list.add(Proxy.NO_PROXY)
                return list
            }
            return proxiesOrNull
        }
        this.eventListener.proxySelectStart(this.call, url)
        this.proxies = selectProxies()
        this.nextProxyIndex = 0
        this.eventListener.proxySelectEnd(this.call, url, this.proxies)
    }

    /** Returns true if there's another proxy to try. */
    private hasNextProxy():boolean{
        return this.nextProxyIndex < this.proxies.length
    }

    /** Returns the next proxy to try. May be PROXY.NO_PROXY but never null. */
    private async nextProxy():Promise<Proxy> {
        if(!this.hasNextProxy()) {
            throw new SocketException(`No route to ${this.address.url.host}; exhausted proxy configurations: ${this.proxies}`)
        }
        let result = this.proxies[this.nextProxyIndex++]
        await this.resetNextInetSocketAddress(result)
        return result
    }

    /** Prepares the socket addresses to attempt for the current proxy or host. */
    private async resetNextInetSocketAddress(proxy:Proxy) {
        // Clear the addresses. Necessary if getAllByName() below throws!
        let mutableInetSocketAddresses = new List<SocketAddress>();
        this.inetSocketAddresses = mutableInetSocketAddresses

        let socketHost: string
        let socketPort: number
        if (proxy.proxyType === ProxyType.DIRECT || proxy.proxyType === ProxyType.SOCKS5) {
            socketHost = this.address.url.host
            socketPort = this.address.url.port
        } else {
            let proxyAddress = proxy.address
            kotlinRequire(proxyAddress instanceof SocketAddress, `Proxy.address() is not an InetSocketAddress: ${typeof proxyAddress}`)
            socketHost = proxyAddress.address
            socketPort = proxyAddress.port
        }

        if (socketPort < 1 || socketPort > 65535) {
            throw new SocketException(`No route to ${socketHost}:${socketPort}; port is out of range`)
        }

        if(proxy.proxyType === ProxyType.SOCKS5) {
            mutableInetSocketAddresses.add(SocketAddress.createUnresolved(socketHost, socketPort))
        } else {
            this.eventListener.dnsStart(this.call, socketHost)

            // Try each address for best behavior in mixed IPv4/IPv6 environments.
            let addresses = await this.address.dns.lookup(socketHost);
            if (addresses.length === 0) {
                throw new UnknownHostException(`${this.address.dns} returned no addresses for $socketHost`)
            }

            this.eventListener.dnsEnd(this.call, socketHost, addresses)

            for (const inetAddress of addresses) {
                mutableInetSocketAddresses.add(new SocketAddress(inetAddress.address, socketPort, inetAddress.family))
            }
        }
    }
}

export class Selection{
    routes: List<Route>
    nextRouteIndex: number = 0

    constructor(routes: List<Route>) {
        this.routes = routes
    }

    hasNext(): boolean {
        return this.nextRouteIndex < this.routes.length
    }

    next(): Route {
        if (!this.hasNext()) {
            throw new NoSuchElementException('No Such Element')
        }
        let result = this.routes[this.nextRouteIndex++]
        return result
    }
}
